<?php

# $Id$
# Copyright 2010, Rasmus Schultz <http://mindplay.dk>

require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'NPSParser.php';

/**
 * Fast Url Resolver (FUR) Compiler.
 * A compiler that translates an NPS configuration file into PHP code.
 */
class FURCompiler
{
  /**
   * @var array all rules, in the order they should be processed.
   * @internal
   */
  public $rules = array();
  
  /**
   * Compiles the given nested rules into PHP code.
   * @return string generated PHP code.
   */
  public function compile($data, $class_name)
  {
    $p = new NPSArrayParser();
    $data = $p->parse($data);
    
    $rules = array();
    
    foreach ($data as $name => $values)
    {
      $rule = $this->createObject($name, $values);
      $rules[$rule->name] = $rule;
    }
    
    $code = '';
    foreach ($rules as $name => $rule)
      $code .= $rule->compile($this);
    
    $code = "public function resolve(\$uri, \$method)\n{\n  \$params = array();\n  \$method = strtoupper(\$method);\n" . FURCompiler::indent($code) . "\n  return false;\n}\n\n";
    
    $code = "public \$routeMap = ".$this->buildRouteMap().";\n\n".$code;
    
    $creators = array();
    foreach ($this->rules as $rule)
      $creators[] = $rule->buildCreator($this);
    $code .= implode("\n", $creators);
    
    $code = "/**\n * File generated with FUR - do not edit!\n */\nclass {$class_name} extends FURBase\n{\n" . FURCompiler::indent($code) . "\n}\n";
    
    return $code;
  }
  
  /**
   * Given a "name.type" and set of values, creates a new rule.
   * @internal
   */
  public function createObject($name, $values, FURCompilerRule $parent=null)
  {
    if (preg_match('/(.+)\.(rule|resource)$/', $name, $matches))
    {
      if (is_array($values))
      {
        $type = 'FURCompiler_'.$matches[2];
        if (!class_exists($type,false))
          throw new Exception('unhandled rule "'.$name.'"');
        return new $type($this, $matches[1], $values, $parent);
      }
    }
    throw new Exception('unhandled rule "'.$name.'"');
  }
  
  /**
   * Helper method, indents a block of code.
   */
  public static function indent($code)
  {
    return "  ".implode("\n  ", explode("\n", $code));
  }
  
  /**
   * Build a map where 'controller/action' => array('route', 'route', ...)
   */
  protected function buildRouteMap()
  {
    $map = array();
    foreach ($this->rules as $rule)
    {
      $key = $rule->controller.'/'.$rule->action;
      if (!isset($map[$key]))
        $map[$key] = array();
      $map[$key][] = $rule->getName();
    }
    
    $code = array();
    foreach ($map as $route => $names)
    {
      $line = array();
      foreach ($names as $name)
        $line[] = var_export($name,true);
      $code[] = var_export($route,true).' => array('.implode(', ', $line).')';
    }
    
    return "array(\n".FURCompiler::indent(implode(",\n",$code))."\n)";
  }
}

/**
 * A base class / interface for types of FURCompiler rules.
 */
abstract class FURCompilerRule
{
  /**
   * Public interface of rules:
   */
  abstract public function __construct(FURCompiler $fur, $name, $values, FURCompilerRule $parent=null);
  abstract public function compile(FURCompiler $fur);
  
  /**
   * @var string local name (as specified in the input file)
   */
  public $name;
  
  /**
   * @var string namespace prefix for nested rules and resources
   */
  public $namespace;
  
  /**
   * @var FURCompilerRule, parent FURCompilerRule instance
   */
  protected $_parent;
  
  /**
   * Common initialization for all rules
   */
  protected function construct(FURCompiler $fur, $name, $values, FURCompilerRule $parent=null)
  {
    $this->name = $name;
    
    if (isset($values['namespace']))
      $this->namespace = $values['namespace'];
    
    $this->_parent = $parent;
    
    $fur->rules[$this->getName()] = $this;
  }
  
  /**
   * Returns the parent FURCompilerRule
   */
  public function getParent()
  {
    return $this->_parent;
  }
  
  /**
   * @return string this rule's name, with the nearest parent
   * namespace applied - if no parent has a defined namespace,
   * the rule's own name is returned.
   */
  public function getName()
  {
    $rule = $this->getParent();
    while ($rule)
    {
      if (isset($rule->namespace))
        return $rule->namespace . '_' . $this->name;
      $rule = $rule->getParent();
    }
    return $this->name;
  }
}

/**
 * This class implements a rule with zero or more nested rules.
 *
 * The FURCompiler class creates instances of this class - you
 * should not manually create instances of this class.
 */
class FURCompiler_rule extends FURCompilerRule
{
  public $pattern;               # the original pattern (as specified in the input file)
  public $controller;            # controller dispatched by this rule
  public $action;                # action dispatched by this rule
  
  protected $_methods;           # array of allowed methods
  protected $_pattern;           # regular expression pattern
  protected $_params;            # parameter name => regular expression subpattern
  protected $_const = array();   # parameter name => constant value
  protected $_rules = array();   # nested rules
  protected $_actions = array(); # array of verb => action
  
  /**
   * Create a new rule, and any nested rules.
   */
  public function __construct(FURCompiler $fur, $name, $values, FURCompilerRule $parent=null)
  {
    $this->construct($fur, $name, $values, $parent);
    
    foreach ($values as $key => $value)
    {
      if (is_array($value))
      {
        $rule = $fur->createObject($key, $value, $this);
        $this->_rules[$rule->name] = $rule;
      }
      else
      {
        $this->$key = $value;
      }
    }
    
    $this->init();
  }
  
  /**
   * Initialize and normalize rule attributes.
   */
  protected function init()
  {
    if (!isset($this->pattern))
      $this->pattern = $this->name;
    
    if (isset($this->route))
    {
      $bits = explode('/', $this->route, 2);
      $this->controller = $bits[0];
      $this->action = isset($bits[1]) ? $bits[1] : 'index';
      unset($this->route);
    }
    
    if (!isset($this->controller))
      $this->controller = $this->name;
    
    if (!isset($this->action))
      $this->action = 'index';
    
    $this->_methods = array();
    if (isset($this->method) || isset($this->methods))
    {
      $methods = isset($this->method) ? $this->method : $this->methods;
      foreach (explode(',', $methods) as $key => $value)
        $this->_methods[] = trim($value);
      unset($this->method);
      unset($this->methods);
    }
    
    foreach ($this as $key => $value)
    {
      // handle action mappings:
      if (preg_match('/^action\.(\w+)$/', $key, $matches))
      {
        $method = strtoupper($matches[1]);
        $this->_actions[$method] = $value;
        
        if (!in_array($method, $this->_methods))
          $this->_methods[] = $method;
        
        unset($this->$key);
      }
      // handle constant parameters:
      if (preg_match('/^@(\w+)$/', $key, $matches))
      {
        $this->_const[$matches[1]] = $value;
        unset($this->$key);
      }
    }
    
    $this->parsePattern();
  }
  
  /**
   * Compile this rule, and any nested rules, into PHP code.
   */
  public function compile(FURCompiler $fur)
  {
    $placeholders = array();
    
    $code = "\n# rule: {$this->name}\n";
    
    $continue = count($this->_rules) ? true : false;
    
    $code .= "if (" . $this->buildCriteria($continue) . ") {\n";
    
    $index = 1;
    foreach ($this->_params as $name => $subpattern)
    {
      if (isset($this->$name) && preg_match('/^\<.*\>$/', $this->$name))
        $placeholders[$name] = "\$matches[" . $index++ . "]";
      else
        $code .= "  \$params[" . var_export($name,true) . "] = \$matches[" . $index++ . "];\n";
    }
    
    foreach ($this->_const as $name => $value)
      $code .= "  \$params[" . var_export($name,true) . "] = " . var_export($value,true) . ";\n";
    
    if ($continue)
      $code .= "  \$uri = \$matches[" . $index . "];\n";
    
    $return = array();
    foreach ($this as $key => $value)
      if (substr($key,0,1)!='_')
        if (!in_array($key, array('pattern','namespace')))
          $return[$key] = var_export($key,true) . '=>' . (isset($placeholders[$key]) ? $placeholders[$key] : var_export($value,true));
    
    $return['name'] = "'name'=>" . var_export($this->getName(),true);
    
    if (count($this->_actions))
    {
      $actions = array();
      foreach ($this->_actions as $method => $action)
        $actions[] = var_export($method,true).'=>'.var_export($action,true);
      $return['action'] = '\'action\'=>$this->mapAction(array(' . implode(',',$actions) .'),$method,' . var_export($this->action,true) . ')';
    }
    
    $code .= "\n  " . ($continue ? "if (\$uri=='') " : "") . "return array(" . implode(',', $return) . ",'params'=>\$params);";
    
    foreach ($this->_rules as $name => $rule)
    {
      $code .= FURCompiler::indent($rule->compile($fur));
    }
    
    if ($continue)
      $code .= "\n  return false;";
    
    $code .= "\n}";
    
    return $code;
  }
  
  /**
   * Build criteria to match this rule
   */
  protected function buildCriteria($continue)
  {
    $suffix = $continue ? '(?=\/.+|$)(?:\/?)(.*)' : '$';
    
    $conditions = array();
    $conditions[] = "preg_match(".var_export('/^'.$this->_pattern.$suffix.'/',true).", \$uri, \$matches)";
    
    if (count($this->_methods))
      $conditions[] = "in_array(\$method, array('".implode("','",$this->_methods)."'))";
    
    return implode(' && ', $conditions);
  }
  
  /**
   * Parse the parameters of the current pattern.
   */
  protected function parsePattern()
  {
    $bits = preg_split(
      '/(\<[^\>]+\>|[^\<]*)/',
      $this->pattern,
      -1,
      PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
    );
    
    $pattern = '';
    $params = array();
    foreach ($bits as $piece)
    { 
      if (preg_match('/^\<(\w+)\:([^\>]+)\>$/', $piece, $parts))
      { // parameter pattern
        $params[$parts[1]] = $parts[2];
        $pattern .= '('.$parts[2].')';
      }
      else
      { // pattern only
        $pattern .= str_replace('/','\/',preg_quote($piece));
      }
    }
    
    $this->_pattern = $pattern;
    $this->_params = $params;
  }
  
  /**
   * Build an URI creator method for this rule.
   * @internal
   */
  public function buildCreator(FURCompiler $fur)
  {
    $params = $this->getAllParams();
    
    $arguments = count($params) ? "(\$" . implode(", \$", array_keys($params)) . ")" : "()";
    
    $code = "function " . $this->getName() . '_uri' . $arguments. "\n{\n  ";
    
    if (count($this->_params))
    {
      $criteria = array();
      foreach ($params as $param => $pattern)
      {
        $criteria[] = "preg_match('/^{$pattern}\$/', \${$param})";
      }
      $code .= "if (" . implode(' && ', $criteria) . ")\n    ";
    }
    
    $code .= "return \"" . preg_replace('/\<(\w+)\:[^\>]+\>/', '{\$\1}', $this->getFullPattern()) . "\";\n";
    
    if (count($params))
      $code .= "  return false;\n";
    
    $code .= "}\n";
    
    if (isset($fur->creators[$this->getName()]))
      throw new Exception("duplicate rule name '".$this->getName()."'");
    
    return $code;
  }
  
  /**
   * Obtain the complete pattern of this rule
   */
  public function getFullPattern()
  {
    $patterns = array();
    $rule = $this;
    while ($rule)
    {
      $patterns[] = $rule->pattern;
      $rule = $rule->getParent();
    }
    return implode('/', array_reverse($patterns));
  }
  
  /**
   * Obtain the parameters
   */
  public function getParams()
  {
    return $this->_params;
  }
  
  /**
   * Obtain the complete set of parameters for this rule
   */
  public function getAllParams()
  {
    $params = array();
    $rule = $this;
    while ($rule)
    {
      $params = $params + array_reverse($rule->getParams());
      $rule = $rule->getParent();
    }
    return array_reverse($params);
  }
}

/**
 * This class specializes the general rule into a nestable resource.
 *
 * The FURCompiler class creates instances of this class - you
 * should not manually create instances of this class.
 */
class FURCompiler_resource extends FURCompilerRule
{
  public $pattern;        # basic resource pattern
  public $controller;     # resource controller name
  public $singular;       # element name (singular)
  public $plural;         # collection name (plural)
  
  protected $_collection; # resource collection rule
  protected $_element;    # resource element rule
  
  /**
   * Create a new resource, and any nested rules or resources.
   */
  public function __construct(FURCompiler $fur, $name, $values, FURCompilerRule $parent=null)
  {
    $this->construct($fur, $name, $values, $parent);
    
    if (isset($values['singular']))
    {
      $this->singular = $values['singular'];
      unset($values['singular']);
    }
    
    if (isset($values['plural']))
    {
      $this->plural = $values['plural'];
      unset($values['plural']);
    }
    
    $this->controller = isset($values['controller']) ? $values['controller'] : $this->name;
    
    $this->pattern = isset($values['pattern']) ? $values['pattern'] : '<id:[0-9]+>';
    
    $this->_collection = $this->createCollection($fur, $values);
    $this->_element = $this->createElement($fur, $values);
  }
  
  /**
   * Create the collection rule for this resource
   */
  protected function createCollection(FURCompiler $fur, $values)
  {
    $values['controller'] = $this->controller;
    $values['pattern'] = (isset($this->plural) ? $this->plural : $this->name);
    
    $values['action.get'] = 'index';
    $values['action.post'] = 'create';
    
    foreach ($values as $key => $value)
      if (is_array($values[$key]))
        unset($values[$key]);
    
    $rule = $fur->createObject((isset($this->plural) ? $this->plural : $this->name.'_set') . '.rule', $values, $this->_parent);
    return $rule;
  }
  
  /**
   * Create the element rule for this resource
   */
  protected function createElement(FURCompiler $fur, $values)
  {
    $values['controller'] = $this->controller;
    $values['pattern'] = (isset($this->plural) ? $this->plural : $this->name) . '/' . $this->pattern;
    
    $values['action.get'] = 'show';
    $values['action.put'] = 'update';
    $values['action.post'] = 'create';
    $values['action.delete'] = 'delete';
    
    $rule = $fur->createObject((isset($this->singular) ? $this->singular : $this->name) . '.rule', $values, $this->_parent);
    return $rule;
  }
  
  /**
   * Compile this resource, and any nested rules or resources, into PHP code.
   */
  public function compile(FURCompiler $fur)
  {
    $code = "\n# resource: {$this->name}\n";
    $code .= $this->_collection->compile($fur) . "\n";
    $code .= $this->_element->compile($fur);
    return $code;
  }
}
